Modern-features
Export and Import
CommonJS (OLD Approach)
- Export
// utils.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = {
add,
multiply
};
- Import
const utils = require('./utils');
console.log(utils.add(2, 3));
console.log(utils.multiply(2, 3));
- Import with object distraction
const { add, multiply } = require('./utils');
ES Modules
Named Export
- Can have multiple named exports per file.
- You import the specific exports you want surrounded in braces
- The name of imported module has to be the same as the name of the exported module. Or you need to use
as
// exports from ./MyComponent.js file
export function MyComponent() {...}
// Alternatively
export const MyComponent = () => {}
export const MyComponent2 = () => {}
// imports
// ex. importing a single named export
import { MyComponent } from "./MyComponent";// ex. importing multiple named exports
import { MyComponent, MyComponent2 } from "./MyComponent";
import * as MainComponents from "./MyComponent";
// use MainComponents.MyComponent and MainComponents.MyComponent2 here
// ex. giving a named import a different name by using "as":
import { MyComponent2 as MyNewComponent } from "./MyComponent";
Default Export
- One can have only one default export per file.
- When we import we have to specify a name.
- Use case:
- Consider using default export for main export such as Button Component in react, and use default export for additional values like constants, utilities, or
ButtonTypes.
- Consider using default export for main export such as Button Component in react, and use default export for additional values like constants, utilities, or
export default function MyComponent() {...}
// Alternatively
const MyComponent = () => {}
export default MyComponent;
// import
import MyDefaultComponent from "./MyDefaultExport";
Dynamic Imports
- Load modules only when they are needed, which is especially useful for large or rarely used libraries.
- Reduces the initial bundle size and improves application startup performance.
async function showCharts() {
if (userWantsCharts) {
const { renderChart } = await import('./chart-library.js');
renderChart(data);
}
}
JavaScript Generators
- A generator is a function that can pause mid-execution, yield a value, and later resume execution from where it left off.
- You create a generator using
function*(note the asterisk) and pause execution with theyieldkeyword.
Use Cases
- Generators are useful when you don't want to generate all values at once (like an array does), but instead produce values on demand (lazily).
- Common use cases include:
- Pagination
- Processing large datasets in manageable chunks
- Infinite sequences
- Custom
iterables - State machines
function* example() {
yield 'A' // Pauses, done: false
yield 'B' // Pauses, done: false
return 'C' // Ends, done: true
}
// With for...of, the return value is ignored.
for (const value of example()) {
console.log(value)
}
// Output:
// A
// B
// (No C)
Custom Objects Can Use Generators
- Generators make implementing the iterator protocol much easier by defining
[Symbol.iterator]().
class Range {
constructor(start, end, step = 1) {
this.start = start
this.end = end
this.step = step
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i += this.step) {
yield i
}
}
}
for (const n of new Range(1, 3)) {
console.log(n)
}
// Output:
// 1
// 2
// 3
Fibonacci Sequence with Generators
- Because generators are lazy, they can create infinite sequences, which is impossible with arrays.
function* fibonacci() {
let prev = 0
let curr = 1
while (true) {
yield curr
const next = prev + curr
prev = curr
curr = next
}
}
const fib = fibonacci()
console.log(fib.next().value) // 1
console.log(fib.next().value) // 1
console.log(fib.next().value) // 2
console.log(fib.next().value) // 3
Pagination (Chunking) with Generators
- Generators can process arrays in small batches instead of all at once.
function* chunk(array, size) {
for (let i = 0; i < array.length; i += size) {
yield array.slice(i, i + size)
}
}
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for (const batch of chunk(data, 3)) {
console.log('Processing batch:', batch)
}
// Output:
// Processing batch: [1, 2, 3]
// Processing batch: [4, 5, 6]
// Processing batch: [7, 8, 9]
// Processing batch: [10]
Simple State Machine
- Generators naturally model state machines because they remember their execution state.
- The generator resumes from where it last paused, making it ideal for state-based workflows.
function* trafficLight() {
while (true) {
yield 'green'
yield 'yellow'
yield 'red'
}
}
const light = trafficLight()
console.log(light.next().value) // "green"
console.log(light.next().value) // "yellow"
console.log(light.next().value) // "red"
console.log(light.next().value) // "green"
Async Generators
- An async generator combines
asyncfunctions with generators. - You can use
awaitinside the generator, and consume the generated values withfor await…of.
async function* fetchUsersAsync() {
const user1 = await fetch('/api/user/1').then(r => r.json())
yield user1
const user2 = await fetch('/api/user/2').then(r => r.json())
yield user2
}
// Consume with for await...of
async function displayUsers() {
for await (const user of fetchUsersAsync()) {
console.log(user.name)
}
}
Proxy
- The Proxy object allows you to create an object that can be used in place of the original object, but which may redefine fundamental Object operations like getting, setting, and defining properties.
- Proxy objects are commonly used to log property accesses, validate, format, or sanitize inputs, and so on.
const target = {
message1: "hello",
message2: "everyone",
};
const handler2 = {
get(target, prop, receiver) {
return "world";
},
};
const proxy2 = new Proxy(target, handler2);
console.log(proxy2.message1); // world
console.log(proxy2.message2); // world
const target = {
message1: "hello",
message2: "everyone",
};
const handler3 = {
get(target, prop, receiver) {
if (prop === "message2") {
return "world";
}
return Reflect.get(...arguments);
},
};
const proxy3 = new Proxy(target, handler3);
console.log(proxy3.message1); // hello
console.log(proxy3.message2); // world
Nullish Coalescing (??)
- Return first value, or second value if first value is
nullorundefined.
const foo = null ?? 'default string';
console.log(foo);
// expected output: "default string"
const baz = 0 ?? 42;
console.log(baz);
// expected output: 0
- Difference with
||
0 ?? "default" -> 0 //left is not null or undefined
0 || "default" -> "default" //left is falsy
false ?? "default" -> false
false || "default" -> "default"
Logical Assignment (??=, ||=)
- Operators to assign a value to a variable based on its own value status.
// Nullish coalescing assignment
// Only assigns if user.name is null or undefined
user.name ??= 'Anonymous';
// Logical OR assignment
// Only assigns if options.debug is falsy
options.debug ||= false;
// Practical example: initializing config
function configure(options = {}) {
options.retries ??= 3;
options.timeout ??= 5000;
options.cache ??= true;
return options;
}
configure({}); // { retries: 3, timeout: 5000, cache: true }
configure({ retries: 0 }); // { retries: 0, timeout: 5000, cache: true }
configure({ timeout: null }); // { retries: 3, timeout: 5000, cache: true }